package com.yifeng.repo.apm.endpoint.logging.logback;

import ch.qos.logback.classic.LoggerContext;
import ch.qos.logback.classic.joran.JoranConfigurator;
import ch.qos.logback.classic.jul.LevelChangePropagator;
import ch.qos.logback.classic.spi.ILoggingEvent;
import ch.qos.logback.classic.util.ContextInitializer;
import ch.qos.logback.core.Appender;
import ch.qos.logback.core.ConsoleAppender;
import ch.qos.logback.core.FileAppender;
import ch.qos.logback.core.joran.spi.JoranException;
import ch.qos.logback.core.status.OnConsoleStatusListener;
import ch.qos.logback.core.status.Status;
import ch.qos.logback.core.util.StatusListenerConfigHelper;
import com.yifeng.repo.apm.endpoint.console.api.model.LogConfTypeEnum;
import com.yifeng.repo.apm.endpoint.sender.LogSenderManager;
import next.fire.spinus.logx.api.model.LevelEnum;
import org.slf4j.ILoggerFactory;
import org.slf4j.bridge.SLF4JBridgeHandler;
import org.slf4j.impl.StaticLoggerBinder;
import org.springframework.boot.logging.LogFile;
import org.springframework.boot.logging.LoggingInitializationContext;
import org.springframework.boot.logging.LoggingSystem;
import org.springframework.boot.logging.LoggingSystemFactory;
import org.springframework.boot.logging.logback.LogbackLoggingSystem;
import org.springframework.boot.logging.logback.LogbackLoggingSystemProperties;
import org.springframework.core.Ordered;
import org.springframework.core.SpringProperties;
import org.springframework.core.annotation.Order;
import org.springframework.core.env.Environment;
import org.springframework.util.Assert;
import org.springframework.util.ClassUtils;
import org.springframework.util.ResourceUtils;

import java.net.URL;
import java.security.CodeSource;
import java.security.ProtectionDomain;
import java.util.ArrayList;
import java.util.Iterator;
import java.util.List;
import java.util.logging.Handler;
import java.util.logging.LogManager;

/**
 * Created by daibing on 2022/10/28.
 *
 * Note: copy from org.springframework.boot.logging.logback.LogbackLoggingSystem (spring-boot-2.6.6)
 */
public class EndpointLogbackLoggingSystem extends LogbackLoggingSystem {

    // Static final field to facilitate code removal by Graal
    private static final boolean XML_ENABLED = !SpringProperties.getFlag("spring.xml.ignore");

    private final boolean localDebug = Boolean.parseBoolean(System.getProperty("log.local.debug"));

    public EndpointLogbackLoggingSystem(ClassLoader classLoader) {
        super(classLoader);
    }

    @Override
    public void beforeInitialize() {
        if (!localDebug) {
            LogSenderManager.startupLogSender();
        }
        super.beforeInitialize();
    }

    @Override
    protected void loadDefaults(LoggingInitializationContext initializationContext, LogFile logFile) {
        LoggerContext context = getLoggerContext();
        stopAndReset(context);
        boolean debug = Boolean.getBoolean("logback.debug");
        if (debug) {
            StatusListenerConfigHelper.addOnConsoleListenerInstance(context, new OnConsoleStatusListener());
        }
        Environment environment = initializationContext.getEnvironment();
        // Apply system properties directly in case the same JVM runs multiple apps
        new LogbackLoggingSystemProperties(environment, context::putProperty).apply(logFile);
        LogbackConfigurator configurator = debug ? new DebugLogbackConfigurator(context)
                : new LogbackConfigurator(context);
        new DefaultLogbackConfiguration(logFile).apply(configurator);
        context.setPackagingDataEnabled(true);

        try {
            if (!localDebug) {
                // 根据日志中心配置重构appender
                refactorAppender(context);
            }
        } catch (Exception e) {
            throw new IllegalStateException("Could not initialize refactor appender ", e);
        }
    }

    @Override
    protected void loadConfiguration(LoggingInitializationContext initializationContext, String location,
                                     LogFile logFile) {
        // 去掉直接调用父类方法, 将父类调用父类的方法体拷贝过来
        // super.loadConfiguration(initializationContext, location, logFile);
        Assert.notNull(location, "Location must not be null");
        if (initializationContext != null) {
            applySystemProperties(initializationContext.getEnvironment(), logFile);
        }

        // 继续改写方法的内容
        LoggerContext loggerContext = getLoggerContext();
        stopAndReset(loggerContext);
        try {
            configureByResourceUrl(initializationContext, loggerContext, ResourceUtils.getURL(location));
            if (!localDebug) {
                // 根据日志中心配置重构appender
                refactorAppender(loggerContext);
            }
        }
        catch (Exception ex) {
            throw new IllegalStateException("Could not initialize Logback logging from " + location, ex);
        }
        List<Status> statuses = loggerContext.getStatusManager().getCopyOfStatusList();
        StringBuilder errors = new StringBuilder();
        for (Status status : statuses) {
            if (status.getLevel() == Status.ERROR) {
                errors.append((errors.length() > 0) ? String.format("%n") : "");
                errors.append(status.toString());
            }
        }
        if (errors.length() > 0) {
            throw new IllegalStateException(String.format("Logback configuration error detected: %n%s", errors));
        }
    }

    private void configureByResourceUrl(LoggingInitializationContext initializationContext, LoggerContext loggerContext,
                                        URL url) throws JoranException {
        if (XML_ENABLED && url.toString().endsWith("xml")) {
            JoranConfigurator configurator = new SpringBootJoranConfigurator(initializationContext);
            configurator.setContext(loggerContext);
            configurator.doConfigure(url);
        }
        else {
            new ContextInitializer(loggerContext).configureByResource(url);
        }
    }

    private void stopAndReset(LoggerContext loggerContext) {
        loggerContext.stop();
        loggerContext.reset();
        if (isBridgeHandlerInstalled()) {
            addLevelChangePropagator(loggerContext);
        }
    }

    private boolean isBridgeHandlerInstalled() {
        if (!isBridgeHandlerAvailable()) {
            return false;
        }
        java.util.logging.Logger rootLogger = LogManager.getLogManager().getLogger("");
        Handler[] handlers = rootLogger.getHandlers();
        return handlers.length == 1 && handlers[0] instanceof SLF4JBridgeHandler;
    }

    private void addLevelChangePropagator(LoggerContext loggerContext) {
        LevelChangePropagator levelChangePropagator = new LevelChangePropagator();
        levelChangePropagator.setResetJUL(true);
        levelChangePropagator.setContext(loggerContext);
        loggerContext.addListener(levelChangePropagator);
    }

    @Override
    public Runnable getShutdownHandler() {
        return () -> {
            this.getLoggerContext().stop();
            if (!localDebug) {
                LogSenderManager.shutdownSender();
            }
        };
    }

    @Override
    public void cleanUp() {
        super.cleanUp();
        // LogSenderManager.shutdownSender();
    }

    private LoggerContext getLoggerContext() {
        ILoggerFactory factory = StaticLoggerBinder.getSingleton().getLoggerFactory();
        Assert.isInstanceOf(LoggerContext.class, factory,
                () -> String.format(
                        "LoggerFactory is not a Logback LoggerContext but Logback is on "
                                + "the classpath. Either remove Logback or the competing "
                                + "implementation (%s loaded from %s). If you are using "
                                + "WebLogic you will need to add 'org.slf4j' to "
                                + "prefer-application-packages in WEB-INF/weblogic.xml",
                        factory.getClass(), getLocation(factory)));
        return (LoggerContext) factory;
    }

    private Object getLocation(ILoggerFactory factory) {
        try {
            ProtectionDomain protectionDomain = factory.getClass().getProtectionDomain();
            CodeSource codeSource = protectionDomain.getCodeSource();
            if (codeSource != null) {
                return codeSource.getLocation();
            }
        }
        catch (SecurityException ex) {
            // Unable to determine location
        }
        return "unknown location";
    }

    /**
     * {@link LoggingSystemFactory} that returns {@link LogbackLoggingSystem} if possible.
     */
    @Order(Ordered.LOWEST_PRECEDENCE - 1)
    public static class Factory implements LoggingSystemFactory {

        private static final boolean PRESENT = ClassUtils.isPresent("ch.qos.logback.classic.LoggerContext",
                LogbackLoggingSystem.Factory.class.getClassLoader());

        @Override
        public LoggingSystem getLoggingSystem(ClassLoader classLoader) {
            if (PRESENT) {
                return new EndpointLogbackLoggingSystem(classLoader);
            }
            return null;
        }

    }

    /**
     * 根据日志中心配置动态修改appender
     */
    private void refactorAppender(LoggerContext loggerContext) {
        this.addAppender(loggerContext);
        this.clearAppender(loggerContext);
    }

    private void addAppender(LoggerContext loggerContext) {
        EndpointLogbackAppender endpointLogbackAppender = new EndpointLogbackAppender();
        endpointLogbackAppender.setName(LogConfTypeEnum.CLOUD.name());
        endpointLogbackAppender.setContext(loggerContext);
        endpointLogbackAppender.start();
        loggerContext.getLogger(LogConfTypeEnum.ROOT.name()).addAppender(endpointLogbackAppender);
        LogSenderManager.getLogSender(true).directPrintLog(LevelEnum.INFO, "EndpointLogbackLoggingSystem-loadConfiguration", "add appender: " + endpointLogbackAppender.getName());
    }

    private void clearAppender(LoggerContext loggerContext) {
        // 检查远程配置
        boolean fileLogEnabled = LogSenderManager.getLogSender(true).isLogEnabled(LogConfTypeEnum.FILE);
        boolean consoleLogEnabled = LogSenderManager.getLogSender(true).isLogEnabled(LogConfTypeEnum.CONSOLE);
        LogSenderManager.getLogSender(true).directPrintLog(LevelEnum.INFO, "EndpointLogbackLoggingSystem-clearAppender",
                "fileLogEnabled=" + fileLogEnabled + ", consoleLogEnabled=" +consoleLogEnabled);
        if (fileLogEnabled && consoleLogEnabled) {
            return;
        }

        // 动态关闭日志配置中的file、console，只做关闭，不做打开
        Iterator<Appender<ILoggingEvent>> it = loggerContext.getLogger(LogConfTypeEnum.ROOT.name()).iteratorForAppenders();
        List<Appender<ILoggingEvent>> clearAppenderList = new ArrayList<>();
        while (it.hasNext()) {
            Appender<ILoggingEvent> appender = it.next();
            if (!fileLogEnabled && appender instanceof FileAppender) {
                appender.stop();
                clearAppenderList.add(appender);
            }
            if (!consoleLogEnabled && appender instanceof ConsoleAppender) {
                appender.stop();
                clearAppenderList.add(appender);
            }
        }
        for (Appender<ILoggingEvent> appender : clearAppenderList) {
            loggerContext.getLogger(LogConfTypeEnum.ROOT.name()).detachAppender(appender);
            LogSenderManager.getLogSender(true).directPrintLog(LevelEnum.INFO, "EndpointLogbackLoggingSystem-clearAppender", "remove appender: " + appender.getName());
        }
    }

}
